package com.cx.repository;

import com.cx.entity.RedisEntity;
import com.cx.service.RedisService;
import com.cx.service.RedisTemplateAdapter;
import com.cx.support.RedisBatch;
import com.cx.support.RedisBatchExecute;
import com.cx.utils.BeanHelper;
import com.cx.wrapper.ListWrapper;
import com.google.common.collect.Lists;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.hash.BeanUtilsHashMapper;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import redis.clients.jedis.Pipeline;

import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.Objects;

public class SecurecyPostProcessor<T extends RedisEntity<ID>, ID extends Serializable> implements RepositoryProxyPostProcessor {

    @Override
	public void postProcess(ProxyFactory factory, RepositoryInformation information) {
		factory.addAdvice(new SecurecyAdvice<T, ID>((Class<T>)information.getDomainType()));
	}

    private class SecurecyAdvice<T extends RedisEntity<ID>, ID extends Serializable> implements MethodInterceptor {

        private final RedisTemplate<String, ?> redisTemplate;

        private final BeanUtilsHashMapper<T> beanUtilsHashMapper;

        private final Class<T> domainClass;

        public SecurecyAdvice(Class<T> domainClass) {
            this.redisTemplate = RedisService.getRedisTemplate(false);
            this.domainClass = domainClass;
            beanUtilsHashMapper = new BeanUtilsHashMapper(domainClass);
        }

        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {

            String methodName = invocation.getMethod().getName();
            int lenth = methodName.length();
            boolean isAnnotationPresent = invocation.getMethod().isAnnotationPresent(Query.class);
            if(methodName.startsWith("findBy") || isAnnotationPresent) {
                String idskey = BeanHelper.key(keyspace(), "findBy", new String[]{methodName.substring(6, lenth)}, invocation.getArguments());
                Type returnType = invocation.getMethod().getGenericReturnType();

                if(returnType.getTypeName().startsWith("java.util.List")){
                    ParameterizedType paramType = (ParameterizedType)returnType;
                    Type[] types = paramType.getActualTypeArguments();
                    if(ArrayUtils.isNotEmpty(types)){
                        if(types[0].getTypeName().equals(domainClass.getTypeName())){
                            List<T> list = findByToRedis(idskey);
                            if (!CollectionUtils.isEmpty(list)) {
                                return list;
                            }
                        }else{
                            ListWrapper listWrapper = RedisService.getObjCache(idskey, ListWrapper.class);//(Class<?>) types[0]
                            if (!ObjectUtils.isEmpty(listWrapper) && !CollectionUtils.isEmpty(listWrapper.getData())) {
                                return listWrapper.getData();
                            }
                        }
                    }else {
                        ListWrapper lw = RedisService.getObjCache(idskey, ListWrapper.class);
                        if (!ObjectUtils.isEmpty(lw) && !CollectionUtils.isEmpty(lw.getData())) {
                            return lw.getData();
                        }
                    }
                } else {
                    if(returnType.getTypeName().equals(domainClass.getTypeName())){
                        List<T> list = findByToRedis(idskey);
                        if (!CollectionUtils.isEmpty(list)) {
                            return list.get(0);
                        }
                    }else{
                        ListWrapper lws = RedisService.getObjCache(idskey, ListWrapper.class);
                        if (!ObjectUtils.isEmpty(lws) && !CollectionUtils.isEmpty(lws.getData())) {
                            return lws.getData().get(0);
                        }
                    }
                }

                Object obj = invocation.proceed();
                if(ObjectUtils.isEmpty(obj)){
                    return obj;
                }
                if(returnType.getTypeName().startsWith("java.util.List")){
                    ParameterizedType paramType = (ParameterizedType)returnType;
                    Type[] types = paramType.getActualTypeArguments();
                    if(ArrayUtils.isNotEmpty(types)){
                        if(types[0].getTypeName().equals(domainClass.getTypeName())){
                            List<T> list = (List<T>)obj;
                            saveFindByToRedis(list, idskey);
                            return obj;
                        }else{
                            List<?> list = (List<?>)obj;
                            RedisService.putObjCache(idskey, new ListWrapper(list));
                            return obj;
                        }
                    }else {
                        List<?> list = (List<?>)obj;
                        RedisService.putObjCache(idskey, new ListWrapper(list));
                        return obj;
                    }
                } else {
                    if(returnType.getTypeName().equals(domainClass.getTypeName())){
                        List<T> list = Lists.newArrayListWithCapacity(1);
                        T t = (T)obj;
                        if(!ObjectUtils.isEmpty(t.getId())) {//防止空对象
                            list.add(t);
                        }
                        saveFindByToRedis(list, idskey);
                        return obj;
                    }else{
                        List<Object> list = Lists.newArrayListWithCapacity(1);
                        list.add(obj);
                        RedisService.putObjCache(idskey, new ListWrapper(list));
                        return obj;
                    }
                }
            }

            return invocation.proceed();
        }

        /**
         *  特殊查询，先缓存key，调用该查询缓存是哪些ID时，先取出对应缓存key的全部ID值，再去缓存找ID KEY类型的所有缓存值
         * @param idskey
         * @return
         */
        public List<T> findByToRedis(String idskey) {
            try {
                List<String> entitykeys = entityKeys(idskey);
                if(!CollectionUtils.isEmpty(entitykeys)) {
                    return getHashDataOfPipelined(entitykeys);
                }

                return null;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        /**
         *  保存返回结果为List<T> findby到redis
         * @return
         */
        public List<T> saveFindByToRedis(List<T> entities, String idskey) {
            if (StringUtils.isBlank(idskey) || CollectionUtils.isEmpty(entities)) {
                return entities;
            }

            if (null == entities.get(0) || null == entities.get(0).getId()) {
                entities.clear();
                return entities;
            }

            try {
                execMultiHashPipelined(entities, idskey);

                return (List<T>) entities;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        private String keyspace(){
            return RedisTemplateAdapter.cacheLevel2Pre() + domainClass.getSimpleName();
        }

        public String key(ID id){
            return keyspace() + ":ids:" + id;
        }

        private List<String> entityKeys(String key){
            Boolean hasKey = redisTemplate.hasKey(key);
            if(BooleanUtils.isFalse(hasKey)){
                return null;
            }

            return RedisService.getListCache(key, String.class);
        }

        private T getOnlyOne(String key){
            Boolean hasKey = redisTemplate.hasKey(key);
            if(BooleanUtils.isFalse(hasKey)){
                return null;
            }
            BoundHashOperations<String, String, String> operations = redisTemplate.boundHashOps(key);
            Map<String, String> entries = operations.entries();
            BeanHelper.registerConvertUtils();
            return beanUtilsHashMapper.fromHash(entries);
        }

        private List<T> getEntities(List<Object> result){
            List<T> list = Lists.newArrayListWithCapacity(result.size());
            result.stream().filter(p->null!=p).forEach(obj -> {
                Map<String, String> map = (Map<String, String>) obj;
                BeanHelper.registerConvertUtils();
                T entity = beanUtilsHashMapper.fromHash(map);
                if (Objects.nonNull(entity) && Objects.nonNull(entity.getId())) {
                    list.add(entity);
                }
            });

            return list;
        }

        private List<T> getHashDataOfPipelined(List<String> entitykeys){
            List<Object> result = RedisService.batchExecute(new RedisBatch() {
                @Override
                public void call(Pipeline pipeline) {
                    entitykeys.forEach(id -> {
                        pipeline.hgetAll(keyspace() + ":ids:" + id);
                    });
                }
            });
            return getEntities(result);

//            List<Object> result = RedisService.executePipelined(new RedisBatchExecute() {
//                @Override
//                public void call(RedisOperations operations) {
//                    entitykeys.forEach(id -> {
//                        BeanHelper.getOnlyOne(keyspace() + ":ids:" + id, operations);
//                    });
//                }
//            });
//            return getEntities(result);
        }

        private void execMultiHashPipelined(List<T> ts, String idskey){
//            RedisService.batchExecute(new RedisBatch() {
//                @Override
//                public void call(Pipeline pipeline) {
//                    List<String> ids = Lists.newArrayListWithCapacity(10);
//                    ts.forEach(t ->
//                            {
//                                ids.add(t.getId()+"");
//                                putAllToCache(pipeline, t);
//                            }
//                    );
//
//                    RedisService.delete(idskey);
//                    RedisService.putListCache(idskey, ids);
//                }
//            });
            RedisService.execute(new RedisBatchExecute() {
                @Override
                public void call(RedisOperations operations) {
                    List<String> ids = Lists.newArrayListWithCapacity(10);
                    ts.forEach(t ->
                            {
                                ids.add(t.getId()+"");
                                putAllToCache(operations, t);
                            }
                    );

                    RedisService.delete(idskey);
                    RedisService.putListCache(idskey, ids);
                }
            }, ts.hashCode()+"");
        }

//        private void putAllToCache(Pipeline pipeline, T t){
//            BeanHelper.registerConvertUtils();
//            Map<String, String> map = beanUtilsHashMapper.toHash(t);
//            pipeline.hmset(key(t.getId()), map);
//        }

        private void putAllToCache(RedisOperations operations, T t){
            BoundHashOperations<String, String, String> hashOperations = operations.boundHashOps(key(t.getId()));
            BeanHelper.registerConvertUtils();
            Map<String, String> map = beanUtilsHashMapper.toHash(t);
            hashOperations.putAll(map);
        }
    }
}